Skip to content

feat(connector): GrabPay connector integration - #2063

Open
peeyushshukla-juspay wants to merge 87 commits into
mainfrom
Grabpay_Connector_Integration
Open

feat(connector): GrabPay connector integration#2063
peeyushshukla-juspay wants to merge 87 commits into
mainfrom
Grabpay_Connector_Integration

Conversation

@peeyushshukla-juspay

@peeyushshukla-juspay peeyushshukla-juspay commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the GrabPay one-time-charge redirect wallet connector with Create Order, OAuth authorization, redirect verification, payment sync, refund, and refund sync support.

Flows Implemented

Flow Service Method Status
Create Order / Authorize types.CompositePaymentService/Authorize Creates the GrabPay charge and returns the OAuth redirect URL
Verify Redirect Response types.CompositePaymentService/VerifyRedirectResponse Exchanges the OAuth code and completes the charge
PSync types.CompositePaymentService/Get Queries the payment status
Refund types.CompositePaymentService/Refund Processes full and partial refunds
RSync types.CompositeRefundService/Get Queries the refund status
Webhook types.EventService/HandleEvent GrabPay webhook handler

Additional Changes

  • This PR modifies the API contract by adding the GrabPay connector, GRAB_PAY payment method type, and grabpayRedirect wallet data.
  • This PR modifies connector configuration by adding GrabPay credentials and base URL configuration.
  • Redirect connector feature data is propagated through the composite flow to preserve PKCE, transaction, currency, and access-token context.

Testing

1. Authorize - Redirect Pending
grpcurl -plaintext \
  -H 'x-connector: grabpay' \
  -H 'x-connector-config: {"config":{"Grabpay":{"partner_id":"<REDACTED>","partner_secret":"<REDACTED>","client_id":"<REDACTED>","client_secret":"<REDACTED>","merchant_id":"<REDACTED>","base_url":"https://partner-api.grab.com/grabpay/partner/v2"}}}' \
  -d '{
    "merchantTransactionId": "<REDACTED_TRANSACTION_ID>",
    "merchantOrderId": "<REDACTED_TRANSACTION_ID>",
    "amount": {
      "minorAmount": 1,
      "currency": "PHP"
    },
    "paymentMethod": {
      "grabpayRedirect": {}
    },
    "captureMethod": "AUTOMATIC",
    "authType": "NO_THREE_DS",
    "returnUrl": "https://example.com/payment/response",
    "webhookUrl": "https://example.com/webhook",
    "description": "GrabPay test payment",
    "address": {
      "billingAddress": {
        "countryAlpha2Code": "PH"
      }
    },
    "testMode": false
  }' \
  localhost:8000 \
  types.CompositePaymentService/Authorize

Response:

{
  "authorizeResponse": {
    "connectorTransactionId": "<REDACTED_TRANSACTION_ID>",
    "status": "AUTHENTICATION_PENDING",
    "statusCode": 302,
    "redirectionData": {
      "uri": {
        "uri": "https://partner-api.grab.com/grabid/v1/oauth2/authorize?<REDACTED>"
      }
    },
    "connectorFeatureData": {
      "value": "{\"state\":\"<REDACTED>\",\"nonce\":\"<REDACTED>\",\"code_verifier\":\"<REDACTED>\",\"redirect_uri\":\"https://example.com/payment/response\",\"partner_tx_id\":\"<REDACTED_TRANSACTION_ID>\",\"currency\":\"PHP\",\"request_code\":\"<REDACTED>\"}"
    }
  },
  "createOrderResponse": {
    "connectorOrderId": "<REDACTED_REQUEST_CODE>",
    "status": "PENDING",
    "statusCode": 200
  },
  "compositeStatus": "COMPLETED"
}
2. Verify Redirect Response - Charge Success
grpcurl -plaintext \
  -H 'x-connector: grabpay' \
  -H 'x-connector-config: {"config":{"Grabpay":{"partner_id":"<REDACTED>","partner_secret":"<REDACTED>","client_id":"<REDACTED>","client_secret":"<REDACTED>","merchant_id":"<REDACTED>","base_url":"https://partner-api.grab.com/grabpay/partner/v2"}}}' \
  -d '{
    "merchantTransactionId": "<REDACTED_TRANSACTION_ID>",
    "merchantOrderId": "<REDACTED_TRANSACTION_ID>",
    "connectorOrderId": "<REDACTED_REQUEST_CODE>",
    "requestDetails": {
      "queryParams": "code=<REDACTED>&state=<REDACTED>"
    },
    "connectorFeatureData": {
      "value": "{\"state\":\"<REDACTED>\",\"nonce\":\"<REDACTED>\",\"code_verifier\":\"<REDACTED>\",\"redirect_uri\":\"https://example.com/payment/response\",\"partner_tx_id\":\"<REDACTED_TRANSACTION_ID>\",\"currency\":\"PHP\",\"request_code\":\"<REDACTED>\"}"
    },
    "amount": {
      "minorAmount": 1,
      "currency": "PHP"
    },
    "paymentMethod": {
      "grabpayRedirect": {}
    },
    "captureMethod": "AUTOMATIC",
    "address": {
      "billingAddress": {
        "countryAlpha2Code": "PH"
      }
    },
    "returnUrl": "https://example.com/payment/response",
    "testMode": false
  }' \
  localhost:8000 \
  types.CompositePaymentService/VerifyRedirectResponse

Response:

{
  "verifyRedirectResponse": {
    "rawConnectorResponse": {
      "value": "{\"code\":\"<REDACTED>\",\"state\":\"<REDACTED>\",\"error\":null}"
    },
    "connectorFeatureData": {
      "value": "<REDACTED_CONNECTOR_FEATURE_DATA>"
    }
  },
  "accessTokenResponse": {
    "accessToken": {
      "value": "<REDACTED_ACCESS_TOKEN>"
    },
    "tokenType": "Bearer",
    "expiresInSeconds": "31535999",
    "status": "OPERATION_STATUS_SUCCESS",
    "statusCode": 200
  },
  "authorizeResponse": {
    "connectorTransactionId": "<REDACTED_CONNECTOR_TRANSACTION_ID>",
    "status": "CHARGED",
    "statusCode": 200,
    "connectorFeatureData": {
      "value": "{\"txID\":\"<REDACTED_CONNECTOR_TRANSACTION_ID>\",\"status\":\"success\",\"paymentMethod\":\"GPWALLET\",\"description\":\"\",\"reason\":\"\"}"
    }
  }
}
3. Payment Sync - Success
grpcurl -plaintext \
  -H 'x-connector: grabpay' \
  -H 'x-connector-config: {"config":{"Grabpay":{"partner_id":"<REDACTED>","partner_secret":"<REDACTED>","client_id":"<REDACTED>","client_secret":"<REDACTED>","merchant_id":"<REDACTED>","base_url":"https://partner-api.grab.com/grabpay/partner/v2"}}}' \
  -d '{
    "connectorTransactionId": "<REDACTED_CONNECTOR_TRANSACTION_ID>",
    "merchantTransactionId": "<REDACTED_TRANSACTION_ID>",
    "state": {
      "accessToken": {
        "token": {
          "value": "<REDACTED_ACCESS_TOKEN>"
        },
        "expiresInSeconds": "31535999",
        "tokenType": "Bearer"
      }
    },
    "amount": {
      "minorAmount": 1,
      "currency": "PHP"
    },
    "paymentMethod": {
      "grabpayRedirect": {}
    },
    "paymentMethodType": "GRAB_PAY",
    "testMode": false
  }' \
  localhost:8000 \
  types.CompositePaymentService/Get

Response:

{
  "getResponse": {
    "connectorTransactionId": "<REDACTED_CONNECTOR_TRANSACTION_ID>",
    "status": "CHARGED",
    "statusCode": 200,
    "amount": {
      "minorAmount": "1",
      "currency": "PHP"
    },
    "connectorFeatureData": {
      "value": "{\"txID\":\"<REDACTED_CONNECTOR_TRANSACTION_ID>\",\"status\":\"success\",\"paymentMethod\":\"GPWALLET\",\"description\":\"\",\"txStatus\":\"success\",\"reason\":\"\"}"
    }
  }
}
4. Refund - Success
grpcurl -plaintext \
  -H 'x-connector: grabpay' \
  -H 'x-connector-config: {"config":{"Grabpay":{"partner_id":"<REDACTED>","partner_secret":"<REDACTED>","client_id":"<REDACTED>","client_secret":"<REDACTED>","merchant_id":"<REDACTED>","base_url":"https://partner-api.grab.com/grabpay/partner/v2"}}}' \
  -d '{
    "merchantRefundId": "<REDACTED_REFUND_ID>",
    "connectorTransactionId": "<REDACTED_TRANSACTION_ID>",
    "connectorOrderId": "<REDACTED_CONNECTOR_TRANSACTION_ID>",
    "paymentAmount": 100,
    "refundAmount": {
      "minorAmount": 1,
      "currency": "PHP"
    },
    "reason": "GrabPay test refund",
    "state": {
      "accessToken": {
        "token": {
          "value": "<REDACTED_ACCESS_TOKEN>"
        },
        "expiresInSeconds": "31535999",
        "tokenType": "Bearer"
      }
    },
    "paymentMethodType": "GRAB_PAY",
    "paymentMethod": {
      "grabpayRedirect": {}
    },
    "testMode": false
  }' \
  localhost:8000 \
  types.CompositePaymentService/Refund

Response:

{
  "refundResponse": {
    "connectorRefundId": "<REDACTED_REFUND_ID>",
    "status": "REFUND_SUCCESS",
    "statusCode": 200,
    "connectorTransactionId": "<REDACTED_TRANSACTION_ID>",
    "rawConnectorResponse": {
      "value": "{\"txID\":\"<REDACTED_CONNECTOR_REFUND_TRANSACTION_ID>\",\"status\":\"success\",\"paymentMethod\":\"GPWALLET\",\"description\":\"\",\"txStatus\":\"success\",\"reason\":\"\",\"echo\":null}"
    }
  }
}
5. Refund Sync - Success
grpcurl -plaintext \
  -H 'x-connector: grabpay' \
  -H 'x-connector-config: {"config":{"Grabpay":{"partner_id":"<REDACTED>","partner_secret":"<REDACTED>","client_id":"<REDACTED>","client_secret":"<REDACTED>","merchant_id":"<REDACTED>","base_url":"https://partner-api.grab.com/grabpay/partner/v2"}}}' \
  -d '{
    "connectorTransactionId": "<REDACTED_TRANSACTION_ID>",
    "refundId": "<REDACTED_REFUND_ID>",
    "connectorRefundId": "<REDACTED_REFUND_ID>",
    "refundAmount": {
      "minorAmount": 1,
      "currency": "PHP"
    },
    "state": {
      "accessToken": {
        "token": {
          "value": "<REDACTED_ACCESS_TOKEN>"
        },
        "expiresInSeconds": "31535999",
        "tokenType": "Bearer"
      }
    },
    "paymentMethodType": "GRAB_PAY",
    "paymentMethod": {
      "grabpayRedirect": {}
    },
    "testMode": false
  }' \
  localhost:8000 \
  types.CompositeRefundService/Get

Response:

{
  "refundResponse": {
    "merchantRefundId": "<REDACTED_REFUND_ID>",
    "connectorRefundId": "<REDACTED_REFUND_ID>",
    "status": "REFUND_SUCCESS",
    "statusCode": 200,
    "connectorTransactionId": "<REDACTED_TRANSACTION_ID>",
    "rawConnectorResponse": {
      "value": "{\"txID\":\"<REDACTED_CONNECTOR_REFUND_TRANSACTION_ID>\",\"status\":\"success\",\"paymentMethod\":\"GPWALLET\",\"description\":\"\",\"txStatus\":\"success\",\"reason\":\"\",\"echo\":null}"
    }
  }
}

Validation

  • cargo fmt --check
  • cargo test -p connector-integration -- grabpay
  • cargo check -p connector-integration -p composite-service -p grpc-server

@peeyushshukla-juspay
peeyushshukla-juspay requested review from a team as code owners August 5, 2026 07:27
peeyushshukla-juspay and others added 25 commits August 5, 2026 13:49
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
hyperswitch-bot Bot and others added 3 commits August 7, 2026 19:19
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Comment thread crates/integrations/connector-integration/src/connectors/grabpay/transformers.rs Outdated
@kanikac199

kanikac199 commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

request_code field (line 202) and its request_code: None init (line 1295) are unused, please drop them.

kanikac199
kanikac199 previously approved these changes Aug 7, 2026
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
Comment thread crates/internal/composite-service/src/payments.rs Outdated
Comment on lines +340 to +352
fn session_token_from_connector_feature_data(
connector_feature_data: Option<&common_utils::pii::SecretSerdeValue>,
) -> Option<String> {
let metadata =
utils::to_connector_meta_from_secret::<serde_json::Value>(connector_feature_data.cloned())
.ok()?;

metadata
.get("session_token")
.or_else(|| metadata.get("access_token"))
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please raise an issue to revert this once Euler side changes for session token is done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already created a ticket 👍

Comment thread crates/integrations/connector-integration/src/connectors/grabpay.rs Outdated
Comment thread crates/integrations/connector-integration/src/connectors/grabpay.rs Outdated
Comment thread crates/integrations/connector-integration/src/connectors/grabpay.rs Outdated
Comment thread crates/integrations/connector-integration/src/connectors/grabpay/transformers.rs Outdated
peeyushshukla-juspay and others added 3 commits August 8, 2026 13:16
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
kanikac199
kanikac199 previously approved these changes Aug 8, 2026
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
});

Ok(Self {
partner_group_tx_id: router_data.request.connector_transaction_id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partnerGroupTxID here is connector_transaction_id — Grab's txID — but the charge sent partnerGroupTxID = partnerTxID, the merchant reference (line 1018). GrabPay groups a refund with its charge by partnerGroupTxID, so these won't link. It also bypasses validate_partner_tx_id, which every other partner-namespace id goes through. Should this come from connector_feature_data.partner_tx_id?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partnerGroupTxID is correctly using the original charge partner namespace id, while originTxID uses Grab's charge txID. This matches GrabPay's refund contract and links the refund to the original charge correctly.

serde_json::json!(response.description),
);
metadata.insert("reason".to_string(), serde_json::json!(response.reason));
if let Some(session_token) = session_token {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The access token stored here gets replayed as the PSync/Refund/RSync bearer by session_token_from_connector_feature_data (grabpay.rs:339). expires_in from the token response is dropped, so a sync or refund after expiry sends a dead token and 401s instead of refreshing. Carry the expiry alongside it and skip the fallback once it's past.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is session_token flow, it is for transaction and does not have expiry. This should ideally be taken from session_token field. Since Euler does not have support for UCS session token flow, i have added this fallback to connector feature data. This will be reverted post Euler UCS bridge support is added. Also this token has an expiry of 1year, hence dropping the expires_in.

Comment thread crates/integrations/connector-integration/src/connectors/grabpay/transformers.rs Outdated
Comment thread crates/integrations/connector-integration/src/connectors/grabpay/transformers.rs Outdated
kanikac199
kanikac199 previously approved these changes Aug 10, 2026
Auto-applied by CI:
- cargo +nightly fmt --all
- make -C sdk generate (if applicable)
- make docs (if applicable)

This commit was automatically generated by GitHub Actions.
@hyperswitch-bot
hyperswitch-bot Bot dismissed stale reviews from Aishwariyaa-Anand and kanikac199 via aa26ff8 August 10, 2026 12:57

@shuklatushar226 shuklatushar226 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check base url once

Comment thread config/sandbox.toml
bypass_urls = ["localhost", "local"]

[connectors]
grabpay.base_url = "https://partner-api.stg-myteksi.com/grabpay/partner/v2"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once confirm dev sandbox and production url

Comment on lines +9 to +11
"MerchantAuthenticationService/CreateServerAuthenticationToken",
"MerchantAuthenticationService/CreateServerSessionAuthenticationToken",
"PaymentService/CreateOrder"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateServerAuthenticationToken and CreateOrder aren't implemented — grabpay.rs registers both under not_implemented, and create_all_prerequisites! declares only Authorize, PSync, Refund, RSync, ServerSessionAuthenticationToken, Authenticate. test_ucs.rs picks suites straight off this list, so both will run and fail. check_connector_specs won't catch it: it flags missing entries, not extra ones.

response: Ok(PaymentsResponseData::TransactionResponse {
resource_id,
redirection_data: None,
connector_metadata: Some(serde_json::json!({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PSync rebuilds connector_metadata from scratch while Authorize merges via build_complete_connector_feature_data. A caller chaining Authorize → PSync → Refund and forwarding the latest connector_feature_data loses session_token (needed by Refund/RSync get_headers) and currency (needed by currency_from_connector_feature_data when refund_money is absent). Can this reuse the merge helper?

.ok_or_else(|| error_stack::report!(errors::WebhookError::WebhookBodyDecodingFailed))?;
status
.trim()
.parse::<T>()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strum's FromStr has no default variant, so any status GrabPay adds later fails here and the webhook is rejected outright. #[serde(other)] Unknown only catches the literal "unknown", which leaves the Unknown => IncomingWebhookEventUnspecified and Unknown => Pending arms unreachable in practice. The HTTP response path degrades gracefully through serde; this one doesn't.

resource_id: None,
status: None,
connector_response_reference_id: None,
error_code: error,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On ?error=access_denied this sets error_code but leaves status: None, and process_composite_verify_redirect_response never checks it. should_do_session_token then returns false (no code), so Authorize runs without a token and dies in get_headers with MissingRequiredField { field_name: "session_token" } — the RPC errors out before the caller ever sees access_denied.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants